﻿/*
	Copyright (C) 2011-2012 Bernardo van der Wal <bernardo@thebigb.nl>

	Permission is hereby granted, free of charge, to any person obtaining a copy of
	this software and associated documentation files (the "Software"), to deal in
	the Software without restriction, including without limitation the rights to
	use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
	of the Software, and to permit persons to whom the Software is furnished to do
	so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in all
	copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
	SOFTWARE.
*/
using System;
using System.Reflection;
using System.Globalization;
using System.Collections.Generic;
using TheBigB.JsonSerializerLite.BuiltinExtensions;

namespace TheBigB.JsonSerializerLite
{
	/// 
	/// Lightweight JSON serializer.
	/// 
	/// Usable methods:
	/// 
	///	 - string Serialize<T>(obj)
	///	   Takes the provided object, retrieves its properties and values, and returns 
	///	   the JSON serialized form of it.
	/// 
	///    NOTE: The following methods are already used internally. They don't have to
	///    be used at all, but may be used for custom tooling.
	/// 
	///	 - string SerializeArray<T>(array)
	///	   Takes the provided array, loops through the items, and returns a JSON 
	///	   compatible array-string for it.
	///	 
	///	 - string SerializeValue(type, value)
	///	   Takes the provided value and returns a JSON compatible string form of it.
	///    
	///  - string EscapeString(str[, cacheString=false])
	///    Takes the provided string and returns a string valid against JSON 
	///    specification. Optionally you can allow this method to cache the input
	///    and output string to improve performance over an instance. You should only
	///    enable cache when you are certain the same string will be escaped more than
	///    once.
	///    
	/// Thread safety:
	///		These methods are not thread-safe. Measures have been taken to make the
	///		caches thread-safe, but seperate instances should be spawned for each 
	///		using thread.
	/// 
	/// Changelog:
	///		2012-04-29
	///			- Copyright updated
	///			- Added new todo's
	///			- Fixed typecast issue. Previous versions couldn't handle objects with 
	///			  embedded objects of other types
	///			- Made Serialization methods non-static for future fixes and features. 
	///			  Added default instance to provide some form of backward 
	///			  compatibility
	///			- Replaced cache dictionaries with thread-safe wrapper
	///			
	///		2012-04-30
	///			- Added more comments and added regions
	///			- Implemented recursion detection
	///			- Added configuration to disable recursion detection
	///			- Added thread safety disclaimer
	///			- Added extension mechanism
	///			- Added two basic tests for the extension mechanism
	///			- Added recursion detection test
	///			
	///		2012-05-01
	///			- Fixed recursion detection test
	///			
	///		2012-05-04
	///			- Discovered flaw in recursion detection. Removed until fixed. The fix
	///			  involves a recursively cloned hashset to check whether a direct 
	///			  ascendant has the same reference. This needs to be done for each 
	///			  branch.
	///			- Added extension capabilities
	/// 
	///		2012-07-30
	///			- Removed recursion detection completely. Recursion detection would
	///			  impact performance too much since it would require recursive deep
	///			  cloning of a tracker object.
	///			  
	///		2012-09-07
	///			- Added escaping for unicode characters
	///			
	///		2012-09-16
	///			- Fixed SerializeArray; multi-type object array weren't being parsed
	///			  correctly
	///			- Fixed Extenders embedded class to allow superclass extensions to
	///			  parse their subclasses
	///			- Added priority delegation on extensions for inherited types
	///			- Added autoloading of built-in extensions: the DateTime and 
	///			  ICollection serializers
	///			
	/// TODOS: 
	///		- Tests to write:
	///			- SerializeArray on an object array with multiple types
	///			- Serialize on a string with unicode characters
	///			- Extension test with superclass effective on subclasses
	///			- Extension test with priority on inheritance level
	/// 
	///		- Maybe a custom IsValueType method to exclude structs and include strings
	///		- Support for multidimensional arrays
	///		- Support for structs (if not supported yet)
	///		- Support for class and property attributes (e.g. XMLInclude)
	///		- Add more builtin extensions
	///			
	/// PROBABLYWONTDOS:
	///		- Optimizations in reflective method invoking (non-generic parameters?)
	///			- Attempted this; appears to be difficult with arrays
	///		- Cache properties for known types 
	///			- Tried this, didn't seem to improve performance
	///

	/// <summary>
	/// Lightweight JSON serializer.
	/// </summary>
	public partial class JsonSerializer
	{
		#region ... Constants ...

		protected const int MAX_KEY_CACHE = 500;
		protected const int MAX_VALUE_CACHE = 5000;

		#endregion

		#region ... Non-public static fields ...

		/// <summary>
		/// Cached culture format (NumberFormatInfo.InvariantInfo is inefficient)
		/// </summary>
		protected static NumberFormatInfo _jsonNumberFormat = NumberFormatInfo.InvariantInfo;

		/// <summary>
		/// Cache for string key escaping.
		/// This cache is used for field names, to prevent overhead with repeated usage of the same types.
		/// </summary>
		protected static Cache<string, string> _keyEscapeCache = new Cache<string, string>(MAX_KEY_CACHE);

		/// <summary>
		/// Cache for string value escaping.
		/// This cache is used for field values, to prevent overhead with repeated usage of the same data.
		/// </summary>
		protected static Cache<string, string> _valueEscapeCache = new Cache<string, string>(MAX_VALUE_CACHE);

		#endregion

		#region ... Public static fields ...

		/// <summary>
		/// Returns a default instance of the JsonSerializer
		/// </summary>
		public static JsonSerializer Default { get; private set; }

		/// <summary>
		/// Gets or sets the maximum cache size for object key escaping.
		/// When set to '0' an the maximum size is unlimited.
		/// </summary>
		public static int KeyEscapeCacheSize 
		{
			get { return _keyEscapeCache.MaxSize; }
			set { _keyEscapeCache.MaxSize = value; }
		}

		/// <summary>
		/// Gets or sets the maximum cache size for object value escaping.
		/// When set to '0' an the maximum size is unlimited.
		/// </summary>
		public static int ValueEscapeCacheSize
		{
			get { return _valueEscapeCache.MaxSize; }
			set { _valueEscapeCache.MaxSize = value; }
		}

		#endregion

		#region ... Non-public fields ...

		/// <summary>
		/// Cached methodinfo for the method Serialize()
		/// </summary>
		private MethodInfo _methodSerialize;

		/// <summary>
		/// Cached methodinfo for the method SerializeArray()
		/// </summary>
		private MethodInfo _methodSerializeArray;

		/// <summary>
		/// Instance of the extenders list
		/// </summary>
		private JsonSerializerExtenders _extenders;

		#endregion

		#region ... Public fields ...

		/// <summary>
		/// Allows you to register and unregister extensions for the serializer.
		/// </summary>
		public JsonSerializerExtenders Extenders 
		{
			get { return this._extenders; } 
			set { /* Does nothing really. Is only here to support '+=' overloading */ } 
		}

		#endregion

		#region ... Constructors ...

		/// <summary>
		/// Static constructor
		/// </summary>
		static JsonSerializer()
		{
			Default = new JsonSerializer();
			KeyEscapeCacheSize = MAX_KEY_CACHE;
			ValueEscapeCacheSize = MAX_VALUE_CACHE;
		}

		/// <summary>
		/// Instance constructor
		/// </summary>
		public JsonSerializer(bool noExtensions = false)
		{
			this._extenders = new JsonSerializerExtenders();

			// Register built-in extensions
			this._extenders += new DateTimeSerializer();
			this._extenders += new ICollectionSerializer();
			
			this._methodSerialize = typeof(JsonSerializer).GetMethod("Serialize");
			this._methodSerializeArray = typeof(JsonSerializer).GetMethod("SerializeArray");
		}

		#endregion
		
		#region ... Public methods ...

		/// <summary>
		/// Returns a JSON formatted string for the provided object.
		/// This method does not accept arrays as input. Use SerializerArray for that purpose.
		/// </summary>
		/// <typeparam name="T">Type of object to serialize</typeparam>
		/// <param name="obj">Object to serialize</param>
		/// <returns>JSON formatted string</returns>
		public string Serialize<T>(T obj)
		{
			Type inputType = typeof(T);

			// If the provided object is null, return null
			if (obj == null)
				return "null";

			// Make sure the provided object is not an array, string or value-type
			if (inputType.IsArray || inputType == typeof(string) || inputType.IsValueType)
				return "{}";

			// If the provided type is handled by an extension we have nothing to do here.
			if (this.Extenders.HasExtension(inputType))
				return this.handleByExtension(inputType, obj);

			// Start a return-buffer with the opening bracket of a JSON object
			string buffer = "{";
			
			// Get properties
			PropertyInfo[] properties = inputType.GetProperties();
			for (int i = 0; i < properties.Length; i++)
			{
				// Add the escaped property name to buffer
				buffer += '"' + EscapeString(properties[i].Name, true) + "\":";

				// Get the property type
				Type propertyType = properties[i].PropertyType;

				if (propertyType.IsValueType || propertyType == typeof(string))
				{
					// Value-type, call SerializeValue method
					buffer += SerializeValue(properties[i].GetValue(obj, null), propertyType);
				}
				else
				{
					MethodInfo targetMethod = this._methodSerialize;
					if (propertyType.IsArray)
					{
						propertyType = propertyType.GetElementType();
						targetMethod = this._methodSerializeArray;
					}

					try
					{
						buffer += targetMethod
							.MakeGenericMethod(propertyType)
							.Invoke(this, new object[] { properties[i].GetValue(obj, null) })
							.ToString();
					}
					catch (TargetInvocationException e)
					{
						throw e.InnerException;
					}
				}

				// Add ',' to split properties if the list of properties is bigger than 1 and the current property isn't the last one
				if (i < properties.Length - 1)
					buffer += ',';
			}

			return buffer + '}';
		}

		/// <summary>
		/// Returns a JSON formatted string for the provided array.
		/// </summary>
		/// <typeparam name="T">Type of array to serialize</typeparam>
		/// <param name="obj">Array to serialize</param>
		/// <returns>JSON formatted string</returns>
		public string SerializeArray<T>(T[] obj)
		{
			// If provide array is null or empty, return the JSON empty array equivelant of "[]"
			if (obj == null || obj.Length == 0)
				return "[]";

			Type inputType = typeof(T);

			// Start array
			string buffer = "[";

			// Loop through array
			for (int i = 0; i < obj.Length; i++)
			{
				Type objType = obj[i].GetType();
				if (this.isStringOrValueType(objType) || this.isStringOrValueType(inputType))
				{
					// Current item is a value-type; retrieve a JSON-compatible string of it
					buffer += SerializeValue(obj[i], objType);
				}
				else
				{
					try
					{
						// Current item is an array or object; retrieve JSON representation of the object.
						// Multidimensional arrays hit a dead-end here. They are not supported yet.
						buffer += this._methodSerialize
							.MakeGenericMethod(obj[i].GetType())
							.Invoke(this, new object[] { obj[i] })
							.ToString();
					}
					catch (TargetInvocationException e)
					{
						throw e.InnerException;
					}
				}

				// Add ',' to split items if the array of items is bigger than 1 and the current item isn't the last one
				if (i < obj.Length - 1)
					buffer += ',';
			}

			// End array and return
			return buffer + "]";
		}

		/// <summary>
		/// Returns a JSON-compatible value string for the provided type.
		/// The string "null" is returned when the provided value type is unsupported or not a value-type (or string).
		/// This method also takes care of escaping strings. Strings should not be escaped prior or afterwards.
		/// </summary>
		/// <param name="value">Value</param>
		/// <param name="type">Type of provided value</param>
		/// <returns>Value string</returns>
		public string SerializeValue(object value, Type type)
		{
			// If provided value is not a value-type or string, return null
			if (!type.IsValueType && type != typeof(string))
				return "null";

			switch (Type.GetTypeCode(type))
			{
				// For booleans return the lowercase string-value of the value
				case TypeCode.Boolean:
					return ((bool)value).ToString().ToLower();

				// Chars and strings are returned escaped, enclosed in double-quotes
				case TypeCode.Char:
				case TypeCode.String:
					return '"' + this.EscapeString(Convert.ToString(value)) + '"';

				// Bytes, ints and longs are returned as simple string
				case TypeCode.Byte:
				case TypeCode.SByte:
				case TypeCode.Int16:
				case TypeCode.Int32:
				case TypeCode.Int64:
				case TypeCode.UInt16:
				case TypeCode.UInt32:
				case TypeCode.UInt64:
					return value.ToString();

				// Decimals, floats and doubles are returned in invariant culture format
				case TypeCode.Decimal:
					return ((decimal)value).ToString(_jsonNumberFormat);
				case TypeCode.Single:
					return ((float)value).ToString(_jsonNumberFormat);
				case TypeCode.Double:
					return ((double)value).ToString(_jsonNumberFormat);

				// For unsupported types return 'null'
				default:
					return "null";
			}
		}

		/// <summary>
		/// Escapes a string to a JSON-compatible format.
		/// The isKey paramater is used for caching purposes as the key-cache 
		/// is likely to be smaller than the value-cache.
		/// </summary>
		/// <param name="str">String to escape</param>
		/// <param name="isValue">Allow caching of string?</param>
		/// <returns>Escaped string</returns>
		public string EscapeString(string str, bool isKey = false)
		{
			if (str == null)
				return "";

			string buffer = "";

			// Check whether the input string already has a processed string in cache
			if (isKey)
			{
				if (_keyEscapeCache.TryGet(str, out buffer))
					return buffer;
			}
			else
			{
				if (_valueEscapeCache.TryGet(str, out buffer))
					return buffer;
			}

			// Loop through each character and escape if nessecary
			for (int i = 0; i < str.Length; i++)
			{
				switch (str[i])
				{
					case '"': buffer += "\\\""; break;
					case '\\': buffer += "\\\\"; break;
					case '/': buffer += "\\/"; break;
					case '\b': buffer += "\\b"; break;
					case '\f': buffer += "\\f"; break;
					case '\n': buffer += "\\n"; break;
					case '\r': buffer += "\\r"; break;
					case '\t': buffer += "\\t"; break;
					default:
						if (str[i] < 32 || str[i] > 255)
							// Escape 32 lower ascii charachters and unicode characters
							buffer += String.Format(@"\u{0:x4}", (int)str[i]);
						else
							// Normal char
							buffer += str[i];
						break;
				}
			}

			// Determine cache type
			if (isKey)
				_keyEscapeCache.Add(str, buffer);
			else
				_valueEscapeCache.Add(str, buffer);

			return buffer;
		}

		#endregion

		#region ... Non-public methods ...

		protected bool isStringOrValueType(Type type)
		{
			return type.IsValueType || type == typeof(string);
		}

		/// <summary>
		/// Lets a user-registered extension parse the provided object.
		/// </summary>
		/// <param name="type">Type of the provided object</param>
		/// <param name="obj">Subject</param>
		/// <returns>JSON representation of object (according to user-extension)</returns>
		protected string handleByExtension(Type type, object obj)
		{
			try
			{
				return this.Extenders.GetExtension(type).Serialize(obj, this);
			}
			catch (Exception e)
			{
				// Extension threw exception, encapsulate and rethrow
				throw new ExtensionException("The extension registered to the type " + type.FullName + " has thrown an uncaught exception", e);
			}
		}

		#endregion

		#region ... Embedded class JsonSerializerExtenders ...

		public class JsonSerializerExtenders
		{
			#region ... Non-public fields ...

			/// <summary>
			/// Holds the list of types and their registered extensions.
			/// </summary>
			private Dictionary<Type, IJsonSerializerExtension> _extensions;

			/// <summary>
			/// Holds a map of relations between searched subclasses and 
			/// their superclasses.
			/// </summary>
			private Dictionary<Type, Type> _relationMap;

			#endregion

			#region ... Constructors ...

			/// <summary>
			/// Instance constructor
			/// </summary>
			public JsonSerializerExtenders()
			{
				this._extensions = new Dictionary<Type, IJsonSerializerExtension>();
				this._relationMap = new Dictionary<Type, Type>();
			}

			#endregion

			#region ... Public methods ...

			/// <summary>
			/// Registers the given extension instance to parse JSON. If provided as argument it 
			/// registers for a specific type. Otherwise the type defined by GetTargetType() is 
			/// used for the registration. If another extension was already registered, the 
			/// registration is overwritten with the new one.
			/// </summary>
			/// <param name="extensionInstance">Instance of the extension</param>
			public void RegisterExtension(IJsonSerializerExtension extensionInstance)
			{
				if (extensionInstance == null)
					return;

				this.RegisterExtension(extensionInstance.GetTargetType(), extensionInstance);
			}

			/// <summary>
			/// Registers the given extension instance to parse JSON. If provided as argument it 
			/// registers for a specific type. Otherwise the type defined by GetTargetType() is 
			/// used for the registration. If another extension was already registered, the 
			/// registration is overwritten with the new one.
			/// </summary>
			/// <param name="type">Type to parse by extension</param>
			/// <param name="extensionInstance">Instance of the extension</param>
			public void RegisterExtension(Type type, IJsonSerializerExtension extensionInstance)
			{
				this.UnregisterExtension(type);

				this._extensions.Add(type, extensionInstance);
				this.rebuildRelationMap();
			}

			/// <summary>
			/// Unregisters the extension configured for the provided type.
			/// </summary>
			/// <param name="type">Type to unregister</param>
			public void UnregisterExtension(Type type)
			{
				// Remove extension
				if (this._extensions.ContainsKey(type))
					this._extensions.Remove(type);

				// Remove all relation mappings
				List<Type> related = new List<Type>();
				foreach (KeyValuePair<Type, Type> kv in this._relationMap)
				{
					if (kv.Value == type)
						related.Add(kv.Key);
				}

				foreach (Type relatedItem in related)
					this._relationMap.Remove(relatedItem);
			}

			#endregion

			#region ... Internal methods ...

			/// <summary>
			/// Returns whether the provided type is registered with an extension instance.
			/// By default the method checks whether there is an extension for a super class
			/// of the given type.
			/// </summary>
			/// <param name="type">Type look up</param>
			/// <param name="ignoreInheritance">Should we check whether there is an extension for a superclass?</param>
			/// <returns>Extension available?</returns>
			internal bool HasExtension(Type type, bool ignoreInheritance = false)
			{
				if (ignoreInheritance)
					// Return true if there is an extension for the exact type (not an ancestor)
					return (this._relationMap.ContainsKey(type) && this._relationMap[type] == type);

				if (this._relationMap.ContainsKey(type))
					// Return true if there is an extension for either the exact type or an ancestor
					// Return false if a null relation is found
					return (this._relationMap[type] != null);

				// Search for ancestor
				Type ancestor = this.findAncestor(type);
				this._relationMap.Add(type, ancestor);

				// Return true if an extension was found for an ancestor
				return (ancestor != null);
			}

			/// <summary>
			/// Returns the extension instance registered to the provided type.
			/// This method returns null if there is no extension registered for the
			/// specified type.
			/// </summary>
			/// <param name="type">Type to retrieve extension for</param>
			/// <returns>Extension instance</returns>
			internal IJsonSerializerExtension GetExtension(Type type)
			{
				if (this._relationMap.ContainsKey(type))
					return this._extensions[this._relationMap[type]];

				// Implicit else

				// Search for ancestor
				Type ancestor = this.findAncestor(type);
				this._relationMap.Add(type, ancestor);

				// Return extension for ancestor
				return this._extensions[ancestor];
			}

			#endregion

			#region ... Private methods ...

			/// <summary>
			/// Checks in the provided type's ancestor tree whether there
			/// is a registered extension for one of them.
			/// Returns null if not found.
			/// </summary>
			/// <param name="searchType">Type to search registered ancestor for</param>
			/// <returns>Registered ancestor</returns>
			private Type findAncestor(Type searchType)
			{
				for (Type current = searchType; current != null; current = current.BaseType)
				{
					if (this._extensions.ContainsKey(current))
						return current;
					
					Type[] interfaces = current.GetInterfaces();
					foreach (Type iface in interfaces)
					{
						if (this._extensions.ContainsKey(iface))
							return iface;
					}
				}
				return null;
			}

			private void rebuildRelationMap()
			{
				this._relationMap.Clear();
				foreach(Type key in this._extensions.Keys)
					this._relationMap.Add(key, key);
			}

			#endregion

			#region ... Operator overloads ...

			/// <summary>
			/// Operator overload to allow '+=' registration of extensions. The targetted type
			/// is detected using the GetTargetType() method.
			/// </summary>
			/// <param name="list"></param>
			/// <param name="extension"></param>
			/// <returns></returns>
			public static JsonSerializerExtenders operator +(JsonSerializerExtenders list, IJsonSerializerExtension extension)
			{
				if (list == null || extension == null || extension.GetTargetType() == null)
					return list;

				list.RegisterExtension(extension.GetTargetType(), extension);
				return list;
			}

			#endregion
		}

		#endregion
	}
}